package classifier;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

import rcscene.Action;
import rcscene.ObjectWeights;
import sceneInfo.FuzzyFramework;
import sceneInfo.FuzzyScene;
import sceneInfo.FuzzySceneLib;
import sceneInfo.Scene;
import weka.classifiers.Classifier;
import weka.classifiers.Evaluation;
//TODO : hopefully this covers all the classifiers
import weka.classifiers.*;

import weka.core.AttributeStats;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.converters.ArffSaver;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.StringTokenizer;

/**
 * Generates classifiers wrt robo cup aspects using weka machine learning,
 * and returns an action with the parameters of that action based on the
 * generated classifiers
 * 
 * @author  Alan Davoust, Wang Xiang
 *
 */
public class RoboClassifier {
	
	//default classifiers to be used
	public static final String DEFACTIONCLASSIFIER = "weka.classifiers.trees.J48";
	public static final String DEFREGRESSIONCLASSIFIER = "weka.classifiers.trees.M5P";
	private String actionClassifierName;
	private String regressionClassifierName;	
	private Classifier actionClassifier;
	private Classifier kickPowerClassifier;
	private Classifier kickDirectionClassifier;
	private Classifier dashPowerClassifier;
	private Classifier turnAngleClassifier;
	private FuzzyFramework framework;
	
	/**
	 * indicates whether we're in evaluation mode (true) or in play mode, in which case we do not evaluate the classifiers
	 */
	private boolean evalmode;
	private float tempEval;
	
	private Instances referenceWekaDataset;
	//object weights to use during play
	private ObjectWeights m_objWeights;
	
	/**Default constructor
	 * 
	 */
	public RoboClassifier (){
		
	}
	
	/** Constructor to build a classifier from a sceneLib
	 * 
	 * @param sceneLib a FuzzySceneLib assuming it has already been discretized 
	 */
	public RoboClassifier (FuzzySceneLib sceneLib, boolean eval){
		this(sceneLib, eval, DEFACTIONCLASSIFIER, DEFREGRESSIONCLASSIFIER);
	}

	public RoboClassifier (FuzzySceneLib sceneLib, boolean eval, String actionClassifierName, String regressionClassifierName){
		evalmode = eval;
		Instances trainingSet = sceneLib.toCompleteWekaDataset();
		this.actionClassifierName = actionClassifierName;
		this.regressionClassifierName = regressionClassifierName;
		
		makeClassifiers(trainingSet);
		framework = sceneLib.getFramework();
		m_objWeights = sceneLib.getObjectWeights();
	}
	
	/** a main method that just builds and tests a classifier*/ 
	public static void main(String[] args) throws Exception{

		if (args.length !=1) {
			System.err.println( "Error : this main method requires a configuration file containing the parameters to evaluate: \n" +
					"usage : java RoboClassifier [config file name]" );
			throw new Exception("Aborted : missing config file.");
		}
		//first argument should be config file
		String configfile = args[0];
		
		
		/// read parameters from file (this is a waste : I copied - pasted the code to parse the parameters from Brain class...
		
		BufferedReader in = new BufferedReader(new FileReader(configfile));
		String str;
		StringTokenizer m_tokenizer;
		
		///////////////////////////////////////

		String inFile= "";
		boolean FuzzType = true; // indicates if we use fuzzy logic or not
		boolean useLog = false; // indicates if we read the trainingset from a log file
		boolean useArff = false;// indicates if we should use an arff file (ignored if useLog==true)
		List<Float> angleParameters = new ArrayList<Float>(); // angle parameters : with the List type we can create the list without knowing ahead of time how many we will have
		List<Float> distanceParameters = new ArrayList<Float>(); // distance paramerters
		ObjectWeights objWeights = new ObjectWeights();
		boolean objw_set = false; // if we set the weights
		String weka1 = DEFACTIONCLASSIFIER; // default action classifier
		String weka2 = DEFREGRESSIONCLASSIFIER; // default regression classifier

		//READING FROM CONFIG FILE/////////////
		while ((str = in.readLine()) != null) //reading lines from config file to get parameter list
		{
			str.trim();
			if (str.startsWith("#")) continue;//comment in config file

			//tokenize line.
			m_tokenizer = new StringTokenizer(str);

			/////////////////////////////////////////////
			// get parameters from line in config file:
			/////////////////////////////////////////////

			//first token is parameter name :
			String paramname = m_tokenizer.nextToken();

			/*valid parameter names are :
			 * LogFile // next token is file name to read logs (if provided, arffFile is ignored)
			 * ArffFile // next token is file name to read arff format trainingset 
			 * AngleParameters // list of angle discretization parameters
			 * DistanceParameters // list of distance discretization parameters
			 * Fuzzy // boolean indicating whether the discretization uses fuzzy logic (use 0 or 1)
			 * ObjectWeights // weights to apply to object types for learning : for now weights accepted are only 0 or 1. 
			 *                 the order of the weights is : ball, teammate, opponent, unknown player, goal, flag, line
			 * ActionClassifier //type of classifier for determining the action (classification task) (full class name with package)
			 * RegressionClassifier //type of classifier to use for regression (determining parameter values)
			 */


			if (paramname.startsWith("LogFile")) // we are given a raw log file
			{
				useLog= true;
				inFile = m_tokenizer.nextToken();

			} else if (paramname.startsWith("ArffFile")) {
				if(!useLog){
					inFile = m_tokenizer.nextToken();
					useArff = true;
				}

			} else if (paramname.startsWith("AngleParam")) {
				//read the angle parameters : there should be nothing else on the line
				while (m_tokenizer.hasMoreTokens()){
					try {
						angleParameters.add(Float.parseFloat(m_tokenizer.nextToken()));
					}
					catch (Exception e){
						System.err.println("reading dist parameters: Exception caught!! " + e.toString());	
					}
				}
			} else if (paramname.startsWith("DistanceParam")) {
				while (m_tokenizer.hasMoreTokens()){
					try {
						distanceParameters.add(Float.parseFloat(m_tokenizer.nextToken()));
					}
					catch (Exception e){
						System.err.println("reading dist parameters: Exception caught!! " + e.toString());	
					}
				}
			} else if (paramname.startsWith("ObjectWeights")) {

				//read weights :ball, teammate, opponent, unknown player, goal, flag, line
				try {
					objWeights.setBallWeight(Float.parseFloat(m_tokenizer.nextToken()));
					objWeights.setTeamPlayerWeight(Float.parseFloat(m_tokenizer.nextToken()));
					objWeights.setOpponentPlayerWeight(Float.parseFloat(m_tokenizer.nextToken()));
					objWeights.setUnknownPlayerWeight(Float.parseFloat(m_tokenizer.nextToken()));
					objWeights.setGoalWeight(Float.parseFloat(m_tokenizer.nextToken()));
					objWeights.setFlagWeight(Float.parseFloat(m_tokenizer.nextToken()));
					objWeights.setLinesWeight(Float.parseFloat(m_tokenizer.nextToken()));
					objw_set = true; // we've set the weights
				}
				catch (Exception e){
					System.err.println("Error Reading Object Weights !! " + e.toString());	
				}
			} else if (paramname.startsWith("Fuzzy")) {
				int Fuzz = Integer.parseInt(m_tokenizer.nextToken());
				if (!((Fuzz== 1)||(Fuzz==0))) // only 0 or 1 at this point
				{
					System.err.println("Error: invalid 'fuzzyness' parameter ("+FuzzType+ "), must be 0 or 1. Reverting to default= 0");
					FuzzType = true;
				}

			} else if (paramname.startsWith("ActionClassifier")) {
				weka1 = m_tokenizer.nextToken();
				System.out.println("Action classifier set to :"+weka1+"|");

			} else if (paramname.startsWith("RegressionClassifier")) {
				weka2 = m_tokenizer.nextToken();
				System.out.println("Regression classifier set to :"+weka2+"|");
			} else {
				System.err.println("Error: invalid parameter name"+paramname);
			}
		} // end while loop reading parameters
		
		// check if we have all the information we need :
		//either a log file or an arff file must be provided
		if(!(useLog || useArff)){
			throw new Exception("Error : no training data provided");
		}
				
		//we have default discretization parameters.
		float [] arrayAngleParam;
		float [] arrayDistanceParam;
		if (angleParameters.size()==0||distanceParameters.size()==0){
			System.out.println("Discretization parameters missing, using default values");
			arrayAngleParam = new float [] {-88.0f, -27.0f, -9.0f, 0.0f,3.0f, 13.0f, 29.0f, 88.0f};
			arrayDistanceParam = new float [] {1.1f, 4.1f, 22.2f, 48.4f,73.0f};
		} else {
			arrayAngleParam = new float[angleParameters.size()];
			for (int i = 0; i<angleParameters.size();i++){
				arrayAngleParam[i]=angleParameters.get(i).floatValue(); //get Float and convert to float
			}
			Arrays.sort(arrayAngleParam); //sort the parameters
			
			arrayDistanceParam = new float[distanceParameters.size()];
			for (int i = 0; i<distanceParameters.size();i++){
				arrayDistanceParam[i]=distanceParameters.get(i).floatValue(); //get Float and convert to float
			}
			Arrays.sort(arrayDistanceParam); //sort the parameters
			 
		}

		// we need object weights : if not provided we use default of 1 for all object types
		if(!objw_set){ 
			System.out.println("no object weights provided, using 1 all around");
			objWeights.setBallWeight(1.0f);
			objWeights.setTeamPlayerWeight(1.0f);
			objWeights.setOpponentPlayerWeight(1.0f);
			objWeights.setUnknownPlayerWeight(1.0f);
			objWeights.setGoalWeight(1.0f);
			objWeights.setFlagWeight(1.0f);
			objWeights.setLinesWeight(1.0f);
		}
		
		
		FuzzyFramework ff= new FuzzyFramework(arrayAngleParam,arrayDistanceParam);;
		

		//create and evaluate the RoboClassifier with these parameters		
		RoboClassifier chuckClassifier;
		if (useLog) {
			FuzzySceneLib sceneLib = new FuzzySceneLib(inFile);
			sceneLib.setFramework(ff);
			sceneLib.setObjectWeights(objWeights);
			sceneLib.discretizeThis(ff);
			chuckClassifier = new RoboClassifier(sceneLib, true, weka1, weka2); // false refers to evaluation or play mode
		} else //using arff
			chuckClassifier = new RoboClassifier(ff, new File(inFile), true,weka1, weka2, objWeights); // get reference to input File object from arff file name 'inFile'
	
		System.out.println("Evaluation : "+ chuckClassifier.evaluate());
		
		
	}

	/**Constructor to build a classifier from a .arff file
	 * 
	 * @param framework the FuzzyFramework instance
	 * @param arffFile .arff file containing the training data
	 * @throws IOException 
	 */
	public RoboClassifier (FuzzyFramework framework, File arffFile, ObjectWeights ow, boolean eval) throws IOException{
		this(framework, arffFile, eval,DEFACTIONCLASSIFIER,DEFREGRESSIONCLASSIFIER , ow);
	}


		/**Constructor to build a classifier from a .arff file
	 * 
	 * @param framework the FuzzyFramework instance
	 * @param arffFile .arff file containing the training data
	 * @param actionClassifierName the class to be used for classifying actions
	 * @param regressionClassifierName the class to be used for estimating parameter values 
	 * @throws IOException 
	 */
	public RoboClassifier (FuzzyFramework framework, File arffFile, boolean eval, String actionClassifierName, String regressionClassifierName, ObjectWeights ow) throws IOException{
		this.framework = framework;
		evalmode = eval;
		BufferedReader data = new BufferedReader (new FileReader(arffFile));
		Instances trainingSet = new Instances(data);
		data.close();
		//set names of classes to be used for classification / regression
		this.actionClassifierName = actionClassifierName;
		this.regressionClassifierName = regressionClassifierName;
		m_objWeights = ow;
		makeClassifiers(trainingSet);
	}

	private void makeClassifiers(Instances trainingSet){
		
		try {

			//Instances actionData = new Instances (trainingSet);
			int numAttri = trainingSet.numAttributes();


			//filter out the data for the parameters and split into 3 separate instances
			//none, dash, kick, turn neck, turn 
			trainingSet.sort(numAttri-3);
			if (!evalmode){ // only do all this if we're in playing mode (as opposed to evaluation mode)
				System.out.println(" Starting training");
				AttributeStats stats = trainingSet.attributeStats(numAttri-3);
				int[] counts = stats.nominalCounts;
				System.out.println(" counts:"  );
				for(int k: counts)System.out.print(k+","  );
				

				Instances dashData = new Instances (trainingSet, counts[Action.ACTION_NONE], 
						counts[Action.ACTION_DASH]);
				Instances kickData = new Instances (trainingSet, counts[Action.ACTION_DASH] + 
						counts[Action.ACTION_NONE], 
						counts[Action.ACTION_KICK]);
				Instances turnData = new Instances (trainingSet, counts[Action.ACTION_NONE] +
						counts[ Action.ACTION_DASH]+
						counts[Action.ACTION_KICK]+
						counts[Action.ACTION_TURNNECK], 
						counts[Action.ACTION_TURN]);

				//build a classifier for dash power attribute
				dashData.deleteAttributeAt(numAttri-1); //remove direction
				dashData.deleteAttributeAt(numAttri-3); //remove actions
				dashData.setClassIndex(numAttri-3);//attribute position shifted to the left

				//We dynamically load the class to do regression 

				Class<?> regressor = Class.forName(regressionClassifierName);//"weka.classifiers.trees.M5P");//regressionClassifierName);//classLoader.loadClass(regressionClassifierName);
				if (Classifier.class.isAssignableFrom(regressor)){ // check if regressor is a subclass of Classifier
					dashPowerClassifier = (Classifier) regressor.newInstance();
				} else {
					throw new Exception("Error : the provided regression classifier class doesn't extend Classifier.");
				}

				dashPowerClassifier.buildClassifier(dashData);

				//build a classifier for kick power attribute
				Instances kickData2 = new Instances (kickData);
				kickData2.deleteAttributeAt(numAttri-1); //remove direction
				kickData2.deleteAttributeAt(numAttri-3); //remove actions
				kickData2.setClassIndex(numAttri-3);
				kickPowerClassifier = (Classifier) regressor.newInstance();
				kickPowerClassifier.buildClassifier(kickData2);

				//build a classifier for kick direction attribute
				kickData.deleteAttributeAt(numAttri-2); //remove power
				kickData.deleteAttributeAt(numAttri-3); //remove actions
				kickData.setClassIndex(numAttri-3);
				//we've already checked that the cast is possible
				kickDirectionClassifier = (Classifier) regressor.newInstance();
				kickDirectionClassifier.buildClassifier(kickData);

				//build a classifier for turn direction attribute
				turnData.deleteAttributeAt(numAttri-2); //remove power
				turnData.deleteAttributeAt(numAttri-3); //remove actions
				turnData.setClassIndex(numAttri-3);
				turnAngleClassifier = (Classifier) regressor.newInstance();
				turnAngleClassifier.buildClassifier(turnData);

			}
			//filters out the data for classify the action
			trainingSet.deleteAttributeAt(numAttri-1);
			trainingSet.deleteAttributeAt(numAttri-2);
			trainingSet.setClassIndex(numAttri-3);

			// sets the trainingSet as the reference format for the Instances we will classify.
			//(we make a copy with no data in it but all the right headers)
			referenceWekaDataset = new Instances(trainingSet,0);


		//Dynamically load the class for Action Classification 
		
		Class<?> classifierclass = Class.forName(actionClassifierName);
	if (Classifier.class.isAssignableFrom(classifierclass)){ // check if we can cast
			actionClassifier = (Classifier) classifierclass.newInstance();
		} else {
			throw new Exception("Error : the provided regression classifier class doesn't extend Classifier.");
		}

		//build a classifier for the actions from the training set
		if (!evalmode){
			System.out.println(" training the action classifier");
			actionClassifier.buildClassifier(trainingSet);
		}
		
		System.out.println((evalmode ? "": "training done")+ (evalmode ? " Starting 10-fold cross-validation": " ready to start ") );
		
		//only in this case does the classifier evaluate itself.
		if (evalmode){
			//Quick and dirty !!
			//evaluate on training data, return f-measure for kick
			Evaluation evaluator = new Evaluation(trainingSet);
			evaluator.crossValidateModel(actionClassifier, trainingSet,10, new Random() );
			//System.out.println("Evaluation summary" + evaluator.toSummaryString());
			//System.out.println("Evaluation :\n"+evaluator.toClassDetailsString());
			System.out.println("Evaluation :confustion matrix\n"+evaluator.toMatrixString());
			
			tempEval = (float) evaluator.fMeasure(Action.ACTION_KICK);
			System.out.println("F-measure for kick : "+evaluator.fMeasure(Action.ACTION_KICK));
		}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	
	public void setObjWeights (ObjectWeights ow){
		m_objWeights = ow;
	}
	//=================================================================
	//************************Methods**********************************
	/**Classify a given scene
	 * 
	 * @param scene the scene to be classified
	 * @return action of the agent, without any parameters if the prediction is not kick, dash, or turn.
	 */
	public Action classify(Scene scene){
		//create the return value/class
		Action act = new Action();
		
		try{
			
		//turn the scene into a FuzzyScene
		FuzzyScene fScene = new FuzzyScene(scene, framework, m_objWeights);
		
		//make instances off the scene
		Instance newScene = fScene.toWekaInstance();
		newScene.setDataset(referenceWekaDataset);
		//System.out.println(" Classifier : weka instance is :" +newScene.toString());
		
		//use actionClassifier to classify (decide which action to perform: kick, dash, turn, or soemthing else)
		double prediction = actionClassifier.classifyInstance(newScene);
		act.setAction((int)(prediction));
		
		//based on the action predicted, classify the parameter(s) for the action
		if (prediction == Action.ACTION_DASH){ //action is dash
			double dashPower = this.dashPowerClassifier.classifyInstance(newScene);
			act.setActionPower(dashPower);
			
		}else if (prediction == Action.ACTION_KICK){ //action is kick
			//filter out power and direction, and then apply classifiers
			double kickPower = kickPowerClassifier.classifyInstance(newScene);
			act.setActionPower(kickPower);

			double kickDirection = kickDirectionClassifier.classifyInstance(newScene);
			act.setActionDirection(kickDirection);
			
		}else if (prediction == Action.ACTION_TURN){ //action is turn
			double turnAngle = turnAngleClassifier.classifyInstance(newScene);
			act.setActionDirection(turnAngle);
			//System.out.println("angleClassifier returns : " + act.toString());
			
		}else{
			//the action classifier has made some other prediction: we don't know how to set the parameters
			return act;
		}
		
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		//System.out.println("classifier returns : " + act.toString());
		return act;
	}
	
	/**Returns the generated classifiers
	 * 
	 * @return the classifiers of the method name
	 */
	public Classifier getActionClassifier(){
		return  actionClassifier;
	}
	
	public Classifier getDashPowerClassifier(){
		return  dashPowerClassifier;
	}
	
	public Classifier getKickPowerClassifier(){
		return  kickPowerClassifier;
	}
	
	public Classifier getKickDirectionClassifier(){
		return  kickDirectionClassifier;
	}
	
	public Classifier getTurnAngleClassifier(){
		return  turnAngleClassifier;
	}
	
	/**Evaluates the accuracy of the classifier
	 * 
	 * @return evaluation of the classifier generated : 
	 * currently the f-measure on KICK of the actionClassifier, obtained by 3-fold cross-validation
	 * @throws Exception 
	 */
	public float evaluate() {
		/*Evaluation eval = new Evaluation(data);
		eval.evaluateModel(classifier, data);
		System.out.print(eval.toSummaryString());
		return 5;*/
		return tempEval;
	}
	
}
